Javascript 是面向对象语言,但不同于 C++、Java 等基于的实现原理,JS 的面向对象和继承都是基于原型链实现的。本文就将深入浅出的对其实现原理进行分析。

# 一、 语言特性

开始剖析原理前,需要记住几个 JS 语言层面的特性:

  • 特性1:函数都是对象,其实引用类型(function、object、array)都是对象; —— 可由 typeof 和 instanceof 验证
  • 特性2:函数都有一个 prototype 属性; —— 属性值是一个对象,称为“原型对象”
  • 特性3:对象都是函数创建的,对象都是属性的集合; —— {} 等形式只是语法糖;
  • 特性4:对象都有一个 __proto__ 属性; —— 即“隐式原型”,属性值指向创建该对象的函数的“原型对象”;

# 二、 函数与对象的关系

上边提到“函数都是对象”、“对象都是函数创建的”,那么二者到底什么关系呢?

一种常见的创建对象的方式如下:

var foo = new Fn()

通常,我们称Fn为构造函数foo为实例对象

通过上述特性可知:
构造函数也是函数,所以它也有一个prototype属性,属性值是一个对象,称为该函数的原型对象
实例对象也是对象,所以它也有一个__proto__属性,属性值也是原型对象
此外,原型对象默认有一个constrcutor属性,指向构造函数

即以下关系是成立的:

  • foo.__proto__ === Fn.prototype
  • Fn.prototype.constructor = Fn

实例对象、构造函数、原型对象 三者间的关系如下图:

微信图片_20200713195715

上图中原型对象(Fn.prototype)也是对象,那么它的 __proto__ 属性值是什么呢?

其实,自定义函数的 prototype 本质上和 var obj = {} 是一样的,都是被 Object 函数创建,所以它的__proto__指向的就是Object.prototype。但Object.prototype比较特殊,它的__proto__指向null。

原型链关系可总结为下图:

181637013624694

很容易得知:console.log(f1 instanceof Foo) // true。instanceof第一个参数是对象,暂时称为A;第二个参数是函数,暂时称为B。
instanceof 的判断规则是:如果 B 的 prototype 出现在 A 的 __proto__ 原型链上,则返回true,否则返回false。

// 实现

    function myInstanceof(left, right) {
        let proto = Object.getPrototypeOf(left)
        let prototype = right.prototype
        while (true) {
            if (proto === null) return false    // 函数中的 return 会提前终止函数,即便 return 在循环中
            if (proto === prototype) return true
            proto = Object.getPrototypeOf(proto)
        }
    }

通过上述规则,可以解析以下怪异现象:

    console.log(Object instanceof Function)     // true
    console.log(Function instanceof Object)     // true
    console.log(Function instanceof Function)   // true

# 三、 原型链与继承

访问一个对象的属性时,先在对象自己的属性中查找,如果没有,再沿着 __proto__ 这条链向上查找,直到 Object.prototype,这就是原型链。所以,js 继承是基于原型链实现的。

如何区分一个属性是自身的还是从原型中找到的呢?可以使用 hasOwnProperty。

如何获取原型? o.__proto__ 或 o.constructor.prototype 或 Object.getPrototypeOf(o)

# 四、 V8 是如何创建对象的

Js 代码在执行时,会被 V8 引擎解析,这时 V8 会用不同的模板来处理 Js 中的对象和函数。

例如:

  • ObjectTemplate 用来创建对象
  • FunctionTemplate 用来创建函数
  • PrototypeTemplate 用来创建函数原型

我们可以得到以下结论:

  • Js 中的函数都是 FunctionTemplate 创建出来的,返回值的是 FunctionTemplate 实例。
  • Js 中的对象都是 ObjectTemplate 创建出来的,返回值的是 ObjectTemplate 实例。
  • Js 中函数的原型(prototype)都是通过 PrototypeTemplate 创建出来的,返回值是 ObjectTemplate 实例。

所以 Js 中的对象的原型可以这样判断:

  • 所有的对象的原型都是 Object.prototype,自定义构造函数的实例除外。
  • 自定义构造函数的实例,它的原型是对应的构造函数原型。

在 Js 中的函数原型判断就更加简单了:

  • 所有的函数原型,都是 Function.prototype。
  • 所有的内置构造函数,他们的原型都是 Function.prototype。

# 五、 几种常见的继承实现

  1. 原型链继承

    本质:将两个原本无关联的构造函数,通过原型链建立起继承关系

     // 父类构造函数
     function Father() {
         this.name = 'father'
     }
     // 子类构造函数
     function Son() {}
     // 要构成继承关系,通过上面的关系图可知,需满足原型链关系 Son.prototype.__proto__ === Father.prototype === (new Father()).__proto__ 也就是如下关系
     Son.prototype = new Father()
     var instance = new Son()
     console.log(instance.name)      // father
    

    缺点:父类中的引用类型属性在子类实例中共用,会导致数据篡改

     function Father() {
         this.like = ['apple']
     }
     function Son() {}
     Son.prototype = new Father()
     var instance_1 = new Son()
     instace_1.like.push('orange')
     var instance_2 = new Son()
     instace_2.like.push('banana')
     console.log(instance_1.like)      // ['apple', 'orange', 'banana']
    
  2. 构造函数继承

    本质:创建子类实例时,都调用一下父类的构造函数,等价于子类实例中完整复制一份父类实例的属性。(因为执行了父类的构造函数,所以是复制父类实例,而不是父类原型)

     function Father() {
         this.like = ['apple']
     }
     function Son() {
         // 每次子类实例化,调用父类构造函数,生成父类实例的副本,复制父类实例属性
         Father.call(this)
     }
     var instance_1 = new Son()
     instace_1.like.push('orange')
     var instance_2 = new Son()
     instace_2.like.push('banana')
     console.log(instance_1.like)        // ['apple', 'orange']
    

    缺点:

    • 只能继承父类实例的属性和方法,不能继承原型的属性和方法。
    • 每次子类实例化,都要在内存中生成一份父类实例的副本,消耗性能。
  3. 组合继承

    本质:组合上述两种方式,用原型链继承实现对原型属性和方法的继承,用构造函数继承实现对父类实例属性和方法的继承。

     function Father() {
         this.like = ['apple']
     }
     function Son() {
         // 构造函数继承
         Father.call(this)   // 第二次调用父类构造函数
     }
     // 原型链继承
     Son.prototype = new Father()    // 第一次调用父类构造函数
     // 重写子类原型的constructor属性,指向自己的构造函数
     Son.prototype.constructor = Son
    

    缺点:

    • 在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法
  4. ES6类继承extends

    ES6引入了class来定义类,并引入了extends来实现继承。本质:先调用父类的构造函数,创建父类的实例对象this,然后再用子类的构造函数来修改this。用法如下:

     class B extends A {
         constructor(x, y, color) {
             super(x, y);    // 调用父类的constructor(x, y)
             this.color = color
         }
     }
    

    存在两条继承链:

    • B.__proto__ = A 作为一个对象,子类的隐式原型是父类。
    • B.prototype.__proto__ = A.__proto__ 作为一个构造函数,子类的显示原型的隐式原型是父类的显示原型。

    extends实现继承的核心代码(寄生组合式继承):

     function _inherits(subType, superType) {
         
         // 创建对象,创建父类原型的一个副本
         // 增强对象,弥补因重写原型而失去的默认的constructor 属性
         // 指定对象,将新创建的对象赋值给子类的原型
         subType.prototype = Object.create(superType && superType.prototype, {
             constructor: {
                 value: subType,
                 enumerable: false,
                 writable: true,
                 configurable: true
             }
         });
         
         if (superType) {
             Object.setPrototypeOf 
                 ? Object.setPrototypeOf(subType, superType) 
                 : subType.__proto__ = superType;
         }
     }
    

    这是最成熟的实现继承的方法。有点:1.只调用了一次SuperType 构造函数,并且因此避免了在SubType.prototype 上创建不必要的、多余的属性。2.原型链还能保持不变,因此,还能够正常使用instanceof 和isPrototypeOf()

# 六、new 做了什么

    var a = new A(a, b);

当这段代码运行的时候,内部实际上执行的是:

    // 1. 创建一个空对象
    var o = {};

    // 2. 将空对象的隐式原型指向为构造器函数的显式原型
    o.__proto__ = A.prototype;  // Object.setPrototypeOf(o, Fn.prototype)
    
    // 3. 将构造函数的this指向空对象,并执行该构造函数将属性和方法添加到空对象上,生成实例对象
    A.call(o, a, b);

注意:构造函数尽量不要返回值。1.无返回值时,会返回创建的实例对象;2.返回原始值时,会忽略该值,并依然返回创建的实例对象;3.返回对象时,new操作符失效,会像执行了函数一样返回该对象。

自己实现 new 操作符:

    // Fn - 构造函数,args - 构造函数的传参
    function myNew(Fn, ...args) {
        let obj = {}
        Object.setPrototypeOf(obj, Fn.prototype)
        let rtn = Fn.apply(obj, args)
        return rtn instanceof Object ? rtn : obj    // 原因见上述“注意”
    }

# 七、 参考

深入理解javascript原型和闭包 (opens new window)

JavaScript常用八种继承方案 (opens new window)

最后更新时间: 6/3/2021, 8:02:18 PM